/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.util;

import java.util.*;

/**
 * Helper to merge lists.<br>
 * You may use this helper to update domain object list with lists of DTOs.<br>
 * Have a look at {@link CrossElementComparator}, it could be usefull for the merging.
 * 
 * @param <T> The content type of the list
 * @author M. Hautle
 */
public class ListMergeHelper<T> {
	/** The merge list. */
	private List<T> merge = new ArrayList<T>();

	/** The comparator used to access {@link #merge}. */
	private final Comparator<T> comparator;

	/**
	 * Default constructor.
	 * 
	 * @param comparator The comparator to use
	 */
	public ListMergeHelper(Comparator<T> comparator) {
		this.comparator = comparator;
	}

	/**
	 * Adds the given elements to the list.
	 * 
	 * @param elements The elements
	 * @return The helper itself
	 */
	public ListMergeHelper<T> addElements(T... elements) {
		for (T e : elements)
			addElement(e);
		return this;
	}

	/**
	 * Adds the given elements to the list.
	 * 
	 * @param elements The elements
	 * @return The helper itself
	 */
	public ListMergeHelper<T> addElements(Collection<T> elements) {
		for (T e : elements)
			addElement(e);
		return this;
	}

	/**
	 * Adds the given element in the list.
	 * 
	 * @param e The element to add
	 */
	public void addElement(T e) {
		int index = Collections.binarySearch(merge, e, comparator);
		if (index < 0)
			index = -index - 1;
		merge.add(index, e);
	}

	/**
	 * Returns the comparator used inside this helper.
	 * 
	 * @return The comparator.
	 */
	public Comparator<T> getComparator() {
		return comparator;
	}

	/**
	 * Returns the size of the list.
	 * 
	 * @return The size
	 */
	public int getSize() {
		return merge.size();
	}

	/**
	 * Appends all elements of the merge list to the passed list.
	 * 
	 * @param list The list
	 * @return The passed list
	 */
	public List<T> appendAllMerged(List<T> list) {
		list.addAll(merge);
		return list;
	}

	/**
	 * Retains all elements of the passed list if they have a corresponding element in this merge list.
	 * 
	 * @param <C> The content type
	 * @param list The list
	 * @return The removed elements of the passed list
	 */
	@SuppressWarnings("unchecked")
	public <C> List<C> retainAllMerged(List<C> list) {
		final List<C> removed = new ArrayList<C>();
		for (int i = list.size() - 1; i > -1; i--) {
			if (Collections.binarySearch(merge, (T) list.get(i), comparator) < 0)
				removed.add(list.remove(i));
		}
		return removed;
	}

	/**
	 * Remove all elements of the passed list having a corresponding element in this merge list.
	 * 
	 * @param <C> The content type
	 * @param list The list
	 * @return The removed elements of the passed list
	 */
	@SuppressWarnings("unchecked")
	public <C> List<C> removeAllMerged(List<C> list) {
		final List<C> removed = new ArrayList<C>();
		for (int i = list.size() - 1; i > -1; i--) {
			if (Collections.binarySearch(merge, (T) list.get(i), comparator) > -1)
				removed.add(list.remove(i));
		}
		return removed;
	}

	/**
	 * Returns the entry for the given keys.<br>
	 * The used comparator must be able to compare the a key array against the list content!
	 * 
	 * @param keys The key values of the entry
	 * @return The corresponding entry or null
	 */
	public T getElementByKeys(Object... keys) {
		return getElement(keys);
	}

	/**
	 * Returns the entry for the key values represented by the given object.<br>
	 * The used comparator must be able to compare the passed argument against the list content!
	 * 
	 * @param o A object representing the key values
	 * @return The corresponding entry or null
	 */
	@SuppressWarnings("unchecked")
	public T getElement(Object o) {
		final int index = Collections.binarySearch(merge, (T) o, comparator);
		return index > -1 ? merge.get(index) : null;
	}

	/**
	 * Removes the entry for the key values represented by the given object.<br>
	 * The passed comparator must be able to compare the passed argument against the list content!
	 * 
	 * @param o A object representing the key values
	 * @return The removed entry or null
	 */
	@SuppressWarnings("unchecked")
	public T removeElement(Object o) {
		final int index = Collections.binarySearch(merge, (T) o, comparator);
		return index > -1 ? merge.remove(index) : null;
	}

	/**
	 * Removes the elements of the given collection.<br>
	 * The passed comparator must be able to compare the passed elements against the list content!
	 * 
	 * @param <C> The element type
	 * @param elements The elements to remove
	 * @return The removed elements
	 */
	public <C> List<T> removeAll(Collection<C> elements) {
		final List<T> rem = new ArrayList<T>(elements.size());
		for (C e : elements) {
			final T r = removeElement(e);
			if (r != null)
				rem.add(r);
		}
		return rem;
	}

	/**
	 * Removes the elements of the given collection.<br>
	 * The passed comparator must be able to compare the passed elements against the list content!
	 * 
	 * @param <C> The element type
	 * @param elements The elements to remove
	 * @return The removed elements
	 */
	@SuppressWarnings("unchecked")
	public <C> List<T> retainAll(Collection<C> elements) {
		final List<T> removed = new ArrayList<T>();
		final int[] retain = new int[elements.size()];
		int cnt = 0;
		// fetch the indices of the elements to retain
		for (C e : elements)
			retain[cnt++] = Collections.binarySearch(merge, (T) e, comparator);
		// order the indices to get the highest one at the end
		Arrays.sort(retain);
		// remove the elements in the 'reverse' order (highest first)
		int i = merge.size() - 1;
		for (int j = retain.length - 1; i > -1 && j > -1 && retain[j] > -1; i--) {
			// is the current index is higher than the index of the next element to retain?
			if (i > retain[j])
				removed.add(merge.remove(i));
			else
				j--;
		}
		// remove all other entries, so the ones having an lower index as the first (existing i.e. not <0) entry in the ret list
		for (; i > -1; i--)
			removed.add(merge.remove(i));
		return removed;
	}

	/**
	 * Invokes the passed handler for the elements in the passed list and in this merge list.<br>
	 * Do not modify those lists inside the passed handler!
	 * 
	 * @param <S> The content type of the list
	 * @param list The list to merge into this one (do not modify it during the merge process!)
	 * @param handler The handler to use
	 */
	@SuppressWarnings("unchecked")
	public <S> void merge(List<S> list, IMergeHandler<T, S> handler) {
		final int[] retain = new int[list.size()];
		int cnt = 0;
		// handle the merged and the added elements
		for (S e : list) {
			final int index = retain[cnt++] = Collections.binarySearch(merge, (T) e, comparator);
			if (index < 0)
				handler.added(e);
			else
				handler.merged(e, merge.get(index));
		}
		// handle the removed elements
		Arrays.sort(retain);
		for (int i = 0, size = merge.size(); i < size; i++) {
			if (Arrays.binarySearch(retain, i) < 0)
				handler.removed(merge.get(i));
		}
	}

	/**
	 * Handler for the merging process ({@link ListMergeHelper#merge(List, IMergeHandler)}).
	 * 
	 * @param <T> The content type of the merge list
	 * @param <S> The content type of the source list
	 * @author M. Hautle
	 */
	public interface IMergeHandler<T, S> {
		/**
		 * Called if a element of the source list does not exists in the merge list.
		 * 
		 * @param el The element of the source list
		 */
		public void added(S el);

		/**
		 * Called if a element of the merge list does not exists in the source list.
		 * 
		 * @param el The element of the merge list
		 */
		public void removed(T el);

		/**
		 * Called if a element of the source list has a corresponding element in the merge list.
		 * 
		 * @param src The element of the source list
		 * @param dst The element of the merge list
		 */
		public void merged(S src, T dst);
	}
}
